package risoe.syslab.fhnew.controller;

/*
 * Copyright (c) 2012-2014, Technical University of Denmark (DTU)
 * All rights reserved.
 * 
 * The Flexhouse 2.0 platform ("this software") is licensed under the
 * BSD 3-clause license.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   - Neither the name of DTU nor the names of its contributors may be used to
 *     endorse or promote products derived from this software without specific
 *     prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE TECHNICAL UNIVERSITY OF DENMARK BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.LinkedList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.w3c.dom.Document;
import org.w3c.dom.svg.SVGDocument;

import risoe.syslab.fhnew.common.RMIUtils;
import risoe.syslab.fhnew.common.SystemConstants.DeviceType;
import risoe.syslab.fhnew.config.BuildingConfig;
import risoe.syslab.fhnew.config.PlatformConfig;
import risoe.syslab.fhnew.gui.DefaultPluginVisualizationHandler;
import risoe.syslab.fhnew.gui.DocumentUtils;
import risoe.syslab.fhnew.gui.VisualizationEvent;
import risoe.syslab.fhnew.gui.VisualizationUpdate;
import risoe.syslab.fhnew.hw.Actuator;
import risoe.syslab.fhnew.hw.Sensor;
import risoe.syslab.model.CompositeMeasurement;
import risoe.syslab.model.CompositeString;
import risoe.syslab.model.flexhouse.ControllerParameter;

/**
 * The Superclass of all plugincontrollers functioning via RMI. When subclassing this controller,
 * remember to register your controller with the controllermanager.
 * @author M. Svendsen
 */
public abstract class PluginController{
	// For default visualization
	public final static String DEFAULT_VISUALIZATION_PATH = "media/pluginDefaultVis.svg";
	private DefaultPluginVisualizationHandler mVisualizationHandler;
	private boolean mUsesDefaultVisualization = false;

	private ControllerManagerRemoteInterface mManagerInterface;
	private BuildingConfig mBuildingConfig = null;
	private PluginInterface mPluginInterface;
	private final String mName;
	private final String mOwner;
	private SVGDocument mDocument;
	private boolean mIsInitialized = false;
	private LinkedList<Object> mQueue = new LinkedList<Object>();

	/**
	 * Instantiates a new PluginController and binds the RMI interface to the given name
	 * @param name a string representing this controller
	 * @param owner a string representing the owner (name or initials) of this PluginController.
	 * @param name of the platform configuration file to use
	 * @throws RemoteException is thrown if no ControllerManager is running
	 * @throws InstantiationException is thrown if another PluginController has already been granted a requested permission,
	 * or if a PluginController bearing the same name already exists
	 * @throws NotBoundException is thrown if the ControllerManager is not bound as an RMI service
	 */
	public PluginController(String name, String owner, String cfgfile) throws RemoteException, InstantiationException {
		mName = name;
		mOwner = owner;

		// Handle missing name argument
		if (name == null || name.isEmpty() || name.trim() == ""){
			throw new InstantiationException("PluginController is missing a proper name");
		}

		// Handle missing owner argument
		if (owner == null || owner.isEmpty() || owner.trim() == ""){
			throw new InstantiationException("PluginController is missing a proper owner. Please specify your name/initials");
		}

		mPluginInterface = new PluginInterface();

		// Start up the RMI interface for this controller
		Registry registry = RMIUtils.getRegistry();
		PluginControllerInterface stub = (PluginControllerInterface)UnicastRemoteObject.exportObject(mPluginInterface, 0);
		registry.rebind(name, stub);

		// Register this controller with the controllermanager
		try{
			String controllerManagerName = new PlatformConfig(cfgfile).getParameter(ControllerManager.CONTROLLER_MANAGER_INTERFACE_KEY, 
					ControllerManager.CONTROLLER_MANAGER_INTERFACE_DEFAULT_NAME);
			mManagerInterface = (ControllerManagerRemoteInterface) registry.lookup(controllerManagerName);
			connectionEstablished();
			mManagerInterface.registerPluginController(name);
		}
		catch (NotBoundException e){
			throw new RemoteException(e.getMessage());
		}

		
		// Print out controller subscriptions and controlled device types
		ControllerConfig config = new ControllerConfig(name, mOwner);
		supplyControllerConfig(config);
		System.out.println("PluginController "+name+" successfully registered with ControllerManager.\nListens to:");
		if (!config.getSubscribedDeviceTypes().isEmpty()){
			for (DeviceType type : config.getSubscribedDeviceTypes()){
				System.out.println("\t"+type.toString());
			}
		}
		else{
			System.out.println("\tNone");
		}
		
		System.out.println("Controls:");
		if (!config.getControlledDeviceTypes().isEmpty()){
			for (DeviceType type : config.getControlledDeviceTypes()){
				System.out.println("\t"+type.toString());
			}
		}
		else{
			System.out.println("\tNone");
		}

		initialStateReceived(mManagerInterface.getActuatorStates());

		// Initialization should now be complete
		mIsInitialized = true;
		for (Object obj : mQueue){
			if (obj instanceof Sensor){
				sensorUpdated((Sensor)obj);
			}
			else{
				actuatorUpdated((Actuator) obj);
			}
		}
		
		// Initialize PluginInterface destructor
		Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
			@Override
			public void run() {
				mPluginInterface.shutdown();
			}
		}));
	}

	/**
	 * Gets the buildingconfig associated with the remote controllermanager
	 * @return a buildingconfig or null if no controllermanager is running
	 */
	public final BuildingConfig getBuildingConfig(){
		// Try to supply the cached BuildingConfig
		if (mBuildingConfig != null){
			return mBuildingConfig;
		}
		try {
			// Cache for later
			return mBuildingConfig = mManagerInterface.getBuildingConfig(); 
		} catch (RemoteException e) {System.out.println("Error getting building config: "+e.getMessage());}
		return null;
	}

	/**
	 * Sends a write request to the ControllerManager. The supplied ControllerConfig must have the devicetype
	 * of this actuator added to it for this request to be allowed. 
	 * @param actuatorName the name of the actuator to which this write must be directed
	 * @param value the value to write
	 */
	public final void setActuator(String actuatorName, CompositeMeasurement value){
		// Notify the default visualization if applicable
		if (mUsesDefaultVisualization){
			mVisualizationHandler.onActuation(actuatorName, value);
		}

		try {
			mManagerInterface.setActuator(mName, actuatorName, value);
		} catch (RemoteException e) {e.printStackTrace();}
	}

	/**
	 * Logs a message on the LoggerManager connected to the ControllerManager
	 * @param type
	 * @param message
	 */
	public final void log(String type, String message){
		try {
			mManagerInterface.log(mName, type, message);
		} catch (RemoteException e) {e.printStackTrace();}
	}

	/**
	 * Query the value of a sensor by name
	 * @param name of the sensor to query
	 * @return a CompositeMeasurement signifying the current value or null if no sensor has the given name
	 */
	public final CompositeMeasurement getSensorValue(String name){
		try {
			return mManagerInterface.getSensorValue(name);
		} catch (RemoteException e) {e.printStackTrace();}
		return null;
	}

	/**
	 * Query the value of an actuator by name
	 * @param name of the actuator to query
	 * @return a CompositeMeasurement signifying the current value or null if no actuator has the given name
	 */
	public final CompositeMeasurement getActuatorValue(String name){
		try {
			return mManagerInterface.getActuatorValue(name);
		} catch (RemoteException e){e.printStackTrace();}
		return null;
	}

	/**
	 * Query the setpoint of an actuator by name
	 * @param name of the actuator to query
	 * @return a CompositeMeasurement signifying the current setpoint or null if no actuator has the given name
	 */
	public final CompositeMeasurement getActuatorSetpoint(String name){
		try {
			return mManagerInterface.getActuatorSetpoint(name);
		} catch (RemoteException e){e.printStackTrace();}
		return null;
	}

	/**
	 * This method is called when the ControllerManager registers this plugincontroller. To correctly implement
	 * a plugincontroller, you must add subscriptions and controller devicetypes to the supplied config, and then return it.
	 * @param config a supplied ControllerConfig.
	 * @return the supplied ControllerConfig with subscriptions and controlled devicetypes added
	 */
	public abstract ControllerConfig supplyControllerConfig(ControllerConfig config);

	/**
	 * USE THIS FOR INITIALIZATION. Once this method has finished, the plugin controller is assumed ready to run.
	 * Called when the controller is first initialized. Provides an array of actuators containing the 
	 * initial state of all actuators in the flexhouse. The controller can then decide if the data should be
	 * logged, reacted upon etc.
	 * @param actuators an array of actuators present in the flexhouse
	 */
	public void initialStateReceived(Actuator[] actuators){};
	
	/**
	 * Convenience callback that happens after having established a connection to the ControllerManager but before
	 * actually registering the PluginController. This can be used for i.e. querying of the building to set up
	 * a custom generated visualization. 
	 */
	public void connectionEstablished() {};

	/**
	 * Called when a subscribed sensor is updated
	 * @param the sensor that caused this event
	 */
	public void sensorUpdated(Sensor s){};

	/**
	 * Called when a subscribed actuator is updated
	 * NOTE: You must override this method in order to receive actuator updates.
	 * @param a the actuator that caused this event
	 */
	public void actuatorUpdated(Actuator a){};

	/**
	 * @return a String representing the name of this plugincontroller.
	 */
	public final String getName(){
		return mName;
	}

	/**
	 * @return a String representing the owner of this PluginController (name or initials)
	 */
	public final String getOwner(){
		return mOwner;
	}

	/**
	 * @return the local representation of the document defining the Batik SVG drawing
	 */
	public final Document getDocument(){
		return mDocument;
	}

	/**
	 * Sends a VisualizationUpdate to the UI Manager using RMI
	 * @param update the VisualizationUpdate to send
	 */
	public final void executeVisualizationUpdate(VisualizationUpdate update){
		// Execute commands on local document first
		// Early exit to avoid errors
		if (mDocument == null) return;

		// Update local documents using the VisualizationUpdate
		DocumentUtils.updateDocument(update, mDocument);

		// Now call interface to apply the commands remotely
		try {
			mManagerInterface.executeVisualizationUpdate(update);
		} catch (RemoteException e) {e.printStackTrace();}
	}

	/**
	 * Called by the Controller Manager when the visualization linked to this plugincontroller
	 * has issued an event.
	 * @param event the event that triggered this call
	 */
	public void onVisualizationEvent(VisualizationEvent event) {};

	public final void onDocumentReady(SVGDocument document){
		mDocument = document;

		// Notify the default visualization if applicable
		if (mUsesDefaultVisualization){
			mVisualizationHandler.documentReady();
		}
	}

	public void controllerParameterUpdated(ControllerParameter parameter, CompositeString value){};

	/**
	 * The actual RMI interface runs asynchronously, meaning that all calls from the ControllerManager
	 * will return instantly.
	 * @author M. Svendsen
	 */
	private class PluginInterface implements PluginControllerInterface{
		private ExecutorService mExecutor = Executors.newSingleThreadExecutor();
		
		/**
		 * Executes a new task on the worker thread of this PluginInterface
		 * @param runnable the task to be run
		 */
		private void execute(Runnable runnable){
			if (!mExecutor.isShutdown()){
				mExecutor.execute(runnable);
			}
		}
		
		/**
		 * Shuts down the worker thread of this PluginInterface
		 */
		public void shutdown(){
			mExecutor.shutdownNow();
		}
		
		@Override
		public void controllerParameterUpdated(final ControllerParameter parameter, final CompositeString value) throws RemoteException {
			execute(new Runnable() {
				@Override
				public void run() {
					try {
						PluginController.this.controllerParameterUpdated(parameter, value);
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			});
		}

		@Override
		public void sensorUpdated(final Sensor sensor) throws RemoteException {
			execute(new Runnable() {
				public void run() {
					try {
						// Notify the default visualization if applicable
						if (mUsesDefaultVisualization){
							mVisualizationHandler.onEvent(sensor);
						} 

						if (mIsInitialized){
							PluginController.this.sensorUpdated(sensor);
						}
						else{
							// Controller is not yet ready to receive events. Queue the event up and notify the controller later
							mQueue.add(sensor);
						}
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			});
		}

		@Override
		public void actuatorUpdated(final Actuator actuator) throws RemoteException {
			execute(new Runnable() {
				@Override
				public void run() {
					try {
						// Notify the default visualization if applicable
						if (mUsesDefaultVisualization){
							mVisualizationHandler.onEvent(actuator);
						}

						if (mIsInitialized){
							PluginController.this.actuatorUpdated(actuator);
						}
						else{
							// Controller is not yet ready to receive events. Queue the event up and notify the controller later
							mQueue.add(actuator);
						}
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			});
		}

		@Override
		public boolean issueHeartBeat() throws RemoteException {
			return true;
		}

		/**
		 * This method makes sure that the supplied ControllerConfig uses the correct name.
		 * Note: This method run synchronously and should be invoked from within a try-catch.
		 */
		@Override
		public final ControllerConfig getControllerConfig() throws RemoteException {
			ControllerConfig conf = supplyControllerConfig(new ControllerConfig(mName, mOwner));
			if (conf.getVisualizationURI() == null && conf.getSerializedVisualization() == null) {
				// Use default visualization
				conf.setVisualizationPath(PluginController.DEFAULT_VISUALIZATION_PATH);
				PluginController.this.mUsesDefaultVisualization = true;
				PluginController.this.mVisualizationHandler = new DefaultPluginVisualizationHandler(PluginController.this);
			}
			
			return conf;
		}

		@Override
		public void onVisualizationEvent(final VisualizationEvent event)
				throws RemoteException {
			execute(new Runnable() {
				@Override
				public void run() {
					try {
						PluginController.this.onVisualizationEvent(event);	
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			});
		}

		@Override
		public void onDocumentReady(final String serializedData) throws RemoteException {
			execute(new Runnable() {
				@Override
				public void run() {
					try {
						// Convert the serialized XML string back into a DOM SVG Document
						SVGDocument doc = DocumentUtils.createDocument(serializedData);
						if (doc != null){
							PluginController.this.onDocumentReady(doc);
						}
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			});
		}
	}
}
